package com.cougil.uoc.seghismed.cipher;

import iaik.asn1.ASN1Object;
import iaik.asn1.CodingException;
import iaik.asn1.DerCoder;
import iaik.asn1.structures.AlgorithmID;
import iaik.pkcs.PKCSException;
import iaik.pkcs.pkcs7.IssuerAndSerialNumber;
import iaik.pkcs.pkcs7.RecipientInfo;
import iaik.pkcs.pkcs7.SignedAndEnvelopedData;
import iaik.pkcs.pkcs7.SignerInfo;
import iaik.x509.X509Certificate;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;

import org.apache.log4j.Logger;

import com.cougil.uoc.seghismed.utils.SeghismedException;
import com.cougil.uoc.seghismed.utils.SeghismedUtils;

/**
 * Classe que permet encapsular la lògica relacionada amb xifrar, desxifrar, signar i verificar la
 * informació signada, emprant la clau privada i el conjunt de certificats per als destinataris associats
 * @author Ignacio Cougil Jares
 * @author <a href="mailto:jcastellar@uoc.edu">Jordi Castella-Roca</a>
 */
public class SignerCipherManager {

	private static final Logger logger = Logger.getLogger(SignerCipherManager.class);

	private byte[] signedCipheredText;
	private byte[] clearText;
	private X509Certificate[] x509ChainSigners;
	private X509Certificate[] x509ChainReceivers;
	private PrivateKey[] privateKeys;
	// private PublicKey publicKey;

	private SignerCipherManager() {
		super();
	}

	/**
	 * Crea una nova instància per xifrar i signar informació emprant la clau privada,
	 * el conjunt de certificats per signar i els certificats dels destinataris informats
	 * @param privateKey Conjunt de claus privades a emprar
	 * @param x509SignersChain Conjunt de certificats a emprar per a la signatura
	 * @param x509ReceviersChain Conjunt de certificats a emprar com a destinataris
	 */
	public SignerCipherManager(PrivateKey[] privateKey, X509Certificate[] x509SignersChain,
			X509Certificate[] x509ReceviersChain) {
		this.setSignersPrivateKey(privateKey);
		this.setSignersCertificateChain(x509SignersChain);
		this.setReceiversCertificateChain(x509ReceviersChain);
	}

	/**
	 * Crea una nova instància per xifrar i signar informació emprant la primera clau privada i el primer
	 * certificat inclòs en el contenidor PKCS#12 emprant el certificat del destinatari informat
	 * @param p12 Contenidor {@link P12} PKCS#12
	 * @param x509 Certificat a emprar com a destinatari
	 */
	public SignerCipherManager(P12 p12, X509Certificate x509) {
		this ( new PrivateKey[] { p12.getPrivateKey() },
				new X509Certificate[] { p12.getCertificates()[0] }, 
				new X509Certificate[] { x509 });
	}

	/**
	 * Crea una nova instància emprada per desxifrar informació emprant el conjunt de claus privades informades
	 * @param privateKey Conjunt de claus privades a emprar
	 */
	public SignerCipherManager(PrivateKey[] privateKey) {
		this.setSignersPrivateKey(privateKey);
	}

	/**
	 * Crea una nova instància emprada per desxifrar informació emprant la clau privada informada
	 * @param privateKey Clau privada a emprar
	 */
	public SignerCipherManager(PrivateKey privateKey) {
		this( new PrivateKey[] { privateKey });
	}


	private void setSignersPrivateKey(PrivateKey[] privateKey) {
		this.privateKeys = privateKey;
	}

	// Optional!
	// public void setPublicKey(PublicKey publicKey)
	// {
	// _publicKey = publicKey;
	// }

	private void setSignersCertificateChain(X509Certificate[] x509Chain) {
		this.x509ChainSigners = x509Chain;
	}

	private void setReceiversCertificateChain(X509Certificate[] x509Chain) {
		this.x509ChainReceivers = x509Chain;
	}

	public byte[] signarXifrar(String clearText) {
		byte[] encrypted = null;
		byte[] clear = SeghismedUtils.getBytes(clearText);
		SignerCipherManager.logger.debug("* Missatge a xifrar i signar: [" + clearText + "] ("
				+ clear.length + " bytes)");
		String xifrat = null;
		if (clearText != null) {
			encrypted = this.signEncrypt(clear);
			xifrat = new String(encrypted);
		}
		SignerCipherManager.logger.debug("* Missatge xifrat i signat: ["+xifrat+"] ("+SeghismedUtils.getBytes(xifrat).length+" bytes)");
		return encrypted;
	}

	private byte[] signEncrypt(byte[] in) {

		this.clearText = new byte[in.length];
		System.arraycopy(in, 0, this.clearText, 0, in.length);

		this.doSignEncrypt();

		return this.signedCipheredText;
	}

	public String verificarDesxifrar(byte[] cipherText) {
		String desxifrat = null;
		String xifrat = SeghismedUtils.getString(cipherText);
		SignerCipherManager.logger.debug("* Missatge a desxifrar i verificar: ["+xifrat+"] ("+SeghismedUtils.getBytes(xifrat).length+" bytes)");
		if (cipherText != null) {
			byte[] decrypted = this.verifyDecrypt(cipherText);
			desxifrat = new String(decrypted);
		}
		SignerCipherManager.logger.debug("* Missatge desxifrat i verificat: [" + desxifrat + "] ("
				+ SeghismedUtils.getBytes(desxifrat).length + " bytes)");
		return desxifrat;
	}

	private byte[] verifyDecrypt(byte[] in) {

		this.signedCipheredText = new byte[in.length];
		System.arraycopy(in, 0, this.signedCipheredText, 0, in.length);

		this.doVerifyDecrypt();

		return this.clearText;
	}

	private void doSignEncrypt() {
		try {
			int i;
			// create a SignedAndEnvelopedData object thereby supplying the data
			// and the content-encryption algorithm to use:
			SignedAndEnvelopedData saed = new SignedAndEnvelopedData(this.clearText,
					AlgorithmID.des_EDE3_CBC);
			// supply the signer\x{00B4}s certificate chain (generally, there may be
			// more than only
			// one signer):
			saed.setCertificates(this.x509ChainSigners);
			// create a SignerInfo structure for the signer entity thereby
			// specifying the signer\x{00B4}s certificate by an
			// IssuerAndSerialNumber
			// structure, the signer\x{00B4}s digest algorithm, and the
			// signer\x{00B4}s private key;
			// the IssuerAndSerialNumber can be created from signer_certs[0], which
			// constitutes
			// the signer\x{00B4}s certificate:
			IssuerAndSerialNumber[] issuer_and_serialNr = new IssuerAndSerialNumber[this.privateKeys.length];
			SignerInfo[] signer_info = new SignerInfo[this.privateKeys.length];

			for (i = 0; i < this.privateKeys.length; i++) {
				issuer_and_serialNr[i] = new IssuerAndSerialNumber(this.x509ChainSigners[i]);
				signer_info[i] = new SignerInfo(issuer_and_serialNr[i],
						AlgorithmID.sha, this.privateKeys[i]);
				// add the SignerInfo just created to the SignedAndEnvelopedData
				// object:
				saed.addSignerInfo(signer_info[i]);
			}

			// create a RecipientInfo for the intended recipient thereby specifiying
			// its certificate (for obtaining the IssuerAndSerialNumber) and the key
			// encryption algorithm to be used (at this time only the rsaEncryption
			// method
			// is supported):
			RecipientInfo[] recipient = new RecipientInfo[this.x509ChainReceivers.length];
			for (i = 0; i < this.x509ChainReceivers.length; i++) {
				recipient[i] = new RecipientInfo(this.x509ChainReceivers[i],
						AlgorithmID.rsaEncryption);
				// add the recipient to the SignedAndEnvelopedData object:
				saed.addRecipientInfo(recipient[i]);
			}
			// prepare the SignedAndEnvelopedData structure for transmission by
			// transforming it to an
			// ASN1Object or immediately DER encoding it:
			// ASN1Object obj = saed.toASN1Object();
			// respectively byte[] encoding = saed.getEncoded();

			this.signedCipheredText = saed.getEncoded();

		} catch (NoSuchAlgorithmException e) {
			throw new SeghismedException(e);
		} catch (PKCSException e) {
			throw new SeghismedException(e);
		}
	}

	private void doVerifyDecrypt() {
		try {
			// if the SignedAndEnvelopedData is supplied as encoding, first decode
			// it to an
			// ASN1Object:
			ASN1Object obj = DerCoder.decode(this.signedCipheredText);
			// create a SignedAndEnvelopedData from the received ASN1 object:
			SignedAndEnvelopedData saed = new SignedAndEnvelopedData(obj);
			// setup the cipher for decryption with the recipient\x{00B4}s private
			// key; perform the
			// encrypted-content decryption
			int recipientInfoIndex = 0;
			saed.setupCipher(this.privateKeys[0], recipientInfoIndex);
			// get the recovered data:
			this.clearText = saed.getContent();
			// verify the signatures of all participated signers:
			SignerInfo[] signer_infos = saed.getSignerInfos();
			for (int i = 0; i < signer_infos.length; i++) {
				// verify the signed data using the SignerInfo at index i
				X509Certificate signer_cert = saed.verify(i);
				// if the signature is OK the certificate of the signer is returned
				SignerCipherManager.logger.info("Signatura OK del signant: "+signer_cert.getSubjectDN());
			}
		} catch (CodingException e) {
			throw new SeghismedException(e);
		} catch (InvalidKeyException e) {
			throw new SeghismedException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new SeghismedException(e);
		} catch (PKCSException e) {
			throw new SeghismedException(e);
		} catch (SignatureException e) {
			throw new SeghismedException(e);
		}
	}

}
